﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;

using Fishpark.PdfWriter.Embedding;
using Fishpark.PdfWriter.Utilities;

namespace Fishpark.PdfWriter.Linearisation
{
    /// <summary>
    /// The PdfLineariser class performs linearisation of a PDF file in one stream, storing the result in another stream.
    /// It is possible for the source document to already be linearised, in which case the operation is superfluous (but will
    /// still result in a linearised output).
    /// It is possible for the source document to be an incorrectly linearised document, and as long as the document is a
    /// valid PDF it will be linearised succesfully.
    /// </summary>
    public class PdfLineariser
    {
        // Private fields
        // ==============
        #region Private fields
        private Stream _input;
        private Stream _output;
        private StreamWriter _streamWriter;
        private PdfLexer _lexer;
        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Linearises the PDF document in the input stream, and writes the linearised version to the output stream.
        /// </summary>
        /// <param name="input">The input stream to read the original PDF from</param>
        /// <param name="output">The output to write the linearised version to</param>
        public void Linearise(Stream input, Stream output)
        {
            if (null == input) throw new ArgumentNullException("input");
            if (null == output) throw new ArgumentNullException("output");

            _input = input;
            _output = output;
            _streamWriter = new StreamWriter(_output, new System.Text.ASCIIEncoding());
            _lexer = new PdfLexer(_input);

            StartLinearisation();
        } // Linearise()
        #endregion



        // Private implemenation
        // =====================
        #region Linearisation
        /// <summary>
        /// Builds up an inventory of the PDF document contained within the source stream.
        /// </summary>
        /// <returns></returns>
        private Inventory BuildInventory()
        {
            Inventory inv = new Inventory();

            // Read the cross-reference of the source document.
            long startXRef = FindStartXRef();
            CrossReferenceTables xref = ReadCrossReferenceTables(startXRef);

            // Parse the objects in the cross-reference table to create the document inventory.
            inv.ParseCrossReferenceTables(xref, _lexer);

            return inv;
        }

        /// <summary>
        /// Completes the linearisation parameters of the output stream. This causes the stream to seek back to the linearisation
        /// parameters dictionary and update its entries.
        /// </summary>
        /// <param name="parameters">The parameters to update.</param>
        private void CompleteLinearisationParameters(LinearisationParameters parameters)
        {
        }

        /// <summary>
        /// Performs linearisation on the input stream, writing the results to the output stream.
        /// </summary>
        private void StartLinearisation()
        {
            // The first thing we do is scan the input document and build up an inventory
            Inventory inventory = BuildInventory();

            // We when write the linearised document based on the inventory.
            WriteLinearisedDocument(inventory);
        }

        /// <summary>
        /// Writes the document-level objects to the output stream, using the specified inventory to determine the objects
        /// that need to be written.
        /// </summary>
        /// <param name="inventory">The inventory</param>
        /// <param name="xref">The cross-reference to update.</param>
        private void WriteDocumentLevelObjects(Inventory inventory, CrossReferenceTables xref)
        {
        }

        /// <summary>
        /// Writes the objects required by the first page to the output stream, using the specified inventory to determine the objects
        /// that need to be written.
        /// </summary>
        /// <param name="inventory">The inventory</param>
        /// <param name="xref">The cross-reference to update.</param>
        private void WriteFirstPage(Inventory inventory, CrossReferenceTables xref)
        {
        }

        /// <summary>
        /// Writes the linearisation parameters dictionary to the output stream. The dictionary is partly based on the
        /// specified inventory but partly indeterminate at this point. A structure is returned with offset within the stream
        /// so that the dictionary can be completed at a later stage.
        /// </summary>
        /// <param name="inventory">The inventory to base the dictionary on</param>
        private LinearisationParameters WriteLinearisationParameters(Inventory inventory)
        {
            Debug.Assert(null != inventory);

            LinearisationParameters parameters = new LinearisationParameters();

            return parameters;
        }

        /// <summary>
        /// Writes a linearised PDF document based on the specified inventory of a source document.
        /// </summary>ggr
        /// <param name="inventory">The inventory</param>
        private void WriteLinearisedDocument(Inventory inventory)
        {
            Debug.Assert(null != inventory);

            CrossReferenceTables xref = new CrossReferenceTables();

            // Part 1: The PDF header
            WritePdfHeader();

            // Part 2: Linearisation parameter dictionary
            LinearisationParameters parameters = WriteLinearisationParameters(inventory);

            // Part 3: Cross-reference for first page (initial pass)
            long xrefLocation = WriteXrefInitialPass(inventory);

            // Part 4: Document catalog and other document-level objects
            WriteDocumentLevelObjects(inventory, xref);

            // Part 6: First-page objects
            WriteFirstPage(inventory, xref);

            // Part 5: Primary hint stream
            WritePrimaryHintStream(inventory, xref);

            // Now we can update the initial cross-reference, and create a new cross-reference for the second part
            // of the document.
            WriteXrefSecondPass(xref, xrefLocation);

            xref = new CrossReferenceTables();

            // Part 7: Remaining pages
            WriteRemainingPages(inventory, xref);

            // Part 8: Shared objects
            WriteSharedObjects(inventory, xref);

            // Part 9: Non-page objects
            WriteNonPageObjects(inventory, xref);

            // Part 11: Main cross-reference and trailer
            WriteXrefComplete(xref);
            WriteTrailer(inventory, xrefLocation);

            // Fill in the linearisation parameters dictionary
            CompleteLinearisationParameters(parameters);

            // Linearisation complete
            _streamWriter.Flush();
        }

        /// <summary>
        /// Writes the PDF header to the output stream.
        /// </summary>
        private void WritePdfHeader()
        {
            _streamWriter.WriteLine("%PDF-1.7");
            _streamWriter.WriteLine("%iiii");
        }

        /// <summary>
        /// Writes the objects that are not associated with any page to the output, based on the specified inventory, and outputs
        /// entries into the supplied cross-reference.
        /// </summary>
        /// <param name="inventory">The inventory to base the hint stream on.</param>
        /// <param name="crossReference">The cross reference to base the hint stream on.</param>
        private void WriteNonPageObjects(Inventory inventory, CrossReferenceTables crossReference)
        {
            Debug.Assert(null != inventory);
            Debug.Assert(null != crossReference);
        }

        /// <summary>
        /// Writes the primary hint stream to the output, based on the inventory and the cross-reference so far.
        /// </summary>
        /// <param name="inventory">The inventory to base the hint stream on.</param>
        /// <param name="crossReference">The cross reference to base the hint stream on.</param>
        private void WritePrimaryHintStream(Inventory inventory, CrossReferenceTables crossReference)
        {
            Debug.Assert(null != inventory);
            Debug.Assert(null != crossReference);
        }

        /// <summary>
        /// Writes the remaining pages to the output, based on the specified inventory, and outputs entries into
        /// the supplied cross-reference.
        /// </summary>
        /// <param name="inventory">The inventory to base the hint stream on.</param>
        /// <param name="crossReference">The cross reference to base the hint stream on.</param>
        private void WriteRemainingPages(Inventory inventory, CrossReferenceTables crossReference)
        {
            Debug.Assert(null != inventory);
            Debug.Assert(null != crossReference);
        }

        /// <summary>
        /// Writes the objects that are shared by all pages (not counting ones that are used by the first page), 
        /// based on the specified inventory, and outputs object entries into the supplied cross-reference.
        /// </summary>
        /// <param name="inventory">The inventory to base the hint stream on.</param>
        /// <param name="crossReference">The cross reference to base the hint stream on.</param>
        private void WriteSharedObjects(Inventory inventory, CrossReferenceTables crossReference)
        {
            Debug.Assert(null != inventory);
            Debug.Assert(null != crossReference);
        }

        /// <summary>
        /// Writes the document trailer. This includes a count of the number of objects in the file, and a link to the initial
        /// cross-reference.
        /// </summary>
        /// <param name="inventory">The inventory to base the trailer on</param>
        /// <param name="xrefLocation">The location within the stream of the initial cross-reference.</param>
        private void WriteTrailer(Inventory inventory, long xrefLocation)
        {
            Debug.Assert(null != inventory);

            _streamWriter.WriteLine("trailer");
            _streamWriter.WriteLine("<<");
            WriteDictionaryEntry("Size", 0); // inventory.NumberOfObjects
            WriteDictionaryEntry("Root", new PdfObjectReference(1, 0)); // ????
            _streamWriter.WriteLine(">>");

            _streamWriter.WriteLine("\r\nstartxref");
            _streamWriter.WriteLine(xrefLocation.ToString());
            _streamWriter.WriteLine("%%EOF");
        }

        /// <summary>
        /// Writes the contents of the specified cross-reference to the output stream.
        /// </summary>
        /// <param name="crossReference">The cross-reference instance that contains the values to write</param>
        private void WriteXrefComplete(CrossReferenceTables crossReference)
        {
            Debug.Assert(null != crossReference);
        }

        private long WriteXrefInitialPass(Inventory inventory)
        {
            Debug.Assert(null != inventory);

            return 0L;
        }

        /// <summary>
        /// Writes the second pass of the initial cross-reference. This fills in the values in each line, based on the
        /// specified cross-reference instance.
        /// </summary>
        /// <param name="crossReference">The cross-reference instance that contains the values to write</param>
        /// <param name="xrefLocation">The location of the cross-reference within the output stream.</param>
        private void WriteXrefSecondPass(CrossReferenceTables crossReference, long xrefLocation)
        {
            Debug.Assert(null != crossReference);
        }
        #endregion

        #region Writing PDF data
        /// <summary>
        /// Writes a new dictionary entry with a key and value, to the document.
        /// No checking is done on the validity of the appearance of a dictionary entry at this stage.
        /// The likely use for this method is to add dictionary entries to an object after a call to OpenObject().
        /// </summary>
        /// <param name="key">The entry's key</param>
        /// <param name="value">The entry's value</param>
        public void WriteDictionaryEntry(string key, object value)
        {
            _streamWriter.WriteLine("\t/{0}\t{1}", key, PdfUtilities.GetPdfStringValue(value));
        } // WriteDictionaryEntry()        
        #endregion

        #region Parsing source cross-reference
        /// <summary>
        /// Finds the start location in the PDF stream of the first xref table.
        /// </summary>
        /// <returns>The location</returns>
        private long FindStartXRef()
        {
            long startXRef = 0L;

            // We start the lexer at the document tail and wait for the appropriate token to be read.
            _lexer.Seek(-128, SeekOrigin.End);

            bool foundStartToken = false;
            while (0 == startXRef && _lexer.Read())
            {
                if (PdfTokenType.Token == _lexer.TokenType || PdfTokenType.Number == _lexer.TokenType)
                    if (true == foundStartToken)
                        startXRef = ConversionUtilities.ReadLong(_lexer.Value);
                    else if ("startxref" == _lexer.Value)
                        foundStartToken = true;
            }

            return startXRef;
        } // FindStartXRef()

        /// <summary>
        /// Reads the cross-reference header.
        /// </summary>
        /// <param name="startIndex">(out) The identifier of the first object in the list.</param>
        /// <param name="length">(out) The number of objects in the list.</param>
        /// <returns>The crossReferences</returns>
        private void ReadCrossReferenceHeader(out int startIndex, out int length)
        {
            startIndex = 0;
            length = 0;
            if ("xref" == _lexer.ReadNextToken())
            {
                startIndex = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
                length = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
            }
        }

        /// <summary>
        /// Reads the cross-reference trailer, and returns the location of the next table in the list.
        /// </summary>
        /// <returns>The location of the next table in the list, or 0 if there is no next table.</returns>
        private long ReadCrossReferenceTrailer()
        {
            long result = 0L;

            bool finished = false;
            while (false == finished && _lexer.Read())
            {
                switch (_lexer.TokenType)
                {
                    case PdfTokenType.DictionaryEnd:
                        finished = true;
                        break;

                    case PdfTokenType.Name:
                        string tokenName = _lexer.Value;
                        switch (tokenName)
                        {
                            case "/Prev":
                                result = ConversionUtilities.ReadLong(_lexer.ReadNextToken());
                                break;

                            case "/Root":
                                int identifier = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
                                int secondary = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
                                _lexer.ReadPastWhitespace();
                                //if (PdfTokenType.R == _lexer.TokenType)
                                //    _documentRoot = new PdfObjectReference(identifier, secondary);
                                break;
                        }
                        break;
                }
            }
            return result;
        } // ReadCrossReferenceTrailer

        /// <summary>
        /// Reads all the cross-reference tables, and returns the crossReferences within.
        /// </summary>
        /// <param name="startXRef">The start of the first cross-reference table.</param>
        /// <returns>The crossReferences</returns>
        private CrossReferenceTables ReadCrossReferenceTables(long startXRef)
        {
            CrossReferenceTables xref = new CrossReferenceTables();

            // We start at the first table.
            long nextTable = startXRef;
            while (nextTable > 0)
            {
                // Find the table
                _lexer.Seek(nextTable, SeekOrigin.Begin);

                // Find out which objects the table contains.
                int startIndex, length;
                ReadCrossReferenceHeader(out startIndex, out length);

                while (startIndex >= 0 && length > 0)
                {
                    // For each table, and each section within a table, we create a new table entry.
                    // (The PDF xref table corresponds to multiple CrossReferenceTable instances)
                    xref.NewTable(startIndex, length);

                    // Read the objects.
                    for (int objectEnum = 0; objectEnum < length; objectEnum++)
                    {
                        long location = ConversionUtilities.ReadLong(_lexer.ReadNextToken());
                        int generation = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
                        string status = _lexer.ReadNextToken();

                        if ("n" == status)
                            xref.Add(new PdfObjectReference(startIndex + objectEnum, generation), location);
                        else
                            xref.Add(PdfObjectReference.Empty, 0L);
                    }

                    // The cross-reference list is either followed by a trailer or by another table.
                    string nextToken = _lexer.ReadNextToken();
                    if ("trailer" == nextToken)
                    {
                        // We find out if there is another table.
                        nextTable = ReadCrossReferenceTrailer();
                        break;
                    }
                    else
                    {
                        startIndex = ConversionUtilities.ReadInt32(nextToken);
                        length = ConversionUtilities.ReadInt32(_lexer.ReadNextToken());
                    }
                }
            }

            return xref;
        } // ReadCrossReferenceTables()
        #endregion
    } // PdfLineariser class
}
